package icasue.tools.compile.utils;

import icasue.tools.compile.compilers.cloader.ClassLoaderCG;
import icasue.reflect.handles.util.OFUtil;
import icasue.tools.compile.compilers.fmanager.JavaFileManagerImpl;
import icasue.tools.compile.compilers.jfobjs.JavaFileObjectGen;
import lombok.extern.slf4j.Slf4j;
import icasue.reflect.handles.exception.ExceptionOF;
import icasue.reflect.handles.object.ObjectOF;
import icasue.reflect.handles.predicate.SureOF;

import javax.tools.*;
import java.io.*;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @Author: Qiao Hang
 * @CreateDate: 2020/12/7 下午5:03
 * @UpdateDate:
 * @Description:
 */
@Slf4j
public class DCUtil {

    public static final ClassLoaderCG classLoaderCG = new ClassLoaderCG();
    private static Pattern compile = Pattern.compile("class\\s+([$_a-zA-Z][$_a-zA-Z0-9]*)\\s*");
    private static volatile String classPathDepend;
    private static Boolean dynamicCompileEnvReady = Boolean.FALSE;

    static { DCUtil.decompress(); dynamicCompileEnvReady = Boolean.TRUE; }

    public static Boolean loadSourceCode(InputStream inputStream){
        String sourceCode = JsonReaderUtil.readStream(inputStream);
        //Using system's compiler.
        JavaCompiler systemJavaCompiler = ToolProvider.getSystemJavaCompiler();
        //Getting a for message's collector, will process in compile.
        DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>();
        //Getting file manager.
        JavaFileManager fileManager = new JavaFileManagerImpl(
                systemJavaCompiler.getStandardFileManager(collector, null, null)
        );

        //Parse source code and patch class name.
        String className = null;
        for (Object[] pro = {null, compile.matcher(sourceCode)}; ObjectOF.isNull_.test(pro[0]); ) {
            Matcher matcher = Matcher.class.cast(pro[1]);
            pro[0] = matcher.find();
            className = matcher.group(1);
        }
        if (ObjectOF.isNull_.test(className)) {
            throw ExceptionOF.runtimeExc_.apply("Can't find className for generic.");
        }else {
            log.info("\n  ######## Dynamic compiler : CLASS_NAME : {}", className);
        }

        //The options for compile.
        List<String> options = new ArrayList<>();
        options.add("-target");
        options.add("1.8");
        if(ObjectOF.notNull_.test(classPathDepend)) {
            options.add("-classpath");
            options.add(classPathDepend);
        }

        //construct JavaFileObject, at this time, the stage is compiler.
        JavaFileObjectGen javaFileObjectGen = new JavaFileObjectGen(className, sourceCode);

        //Invoke system compiler, do compile
        //In this stage, system compiler will invoke JavaFileObject's getContent method, read resource code and do compile!
        //After compile, the process flag will change Kind from SOURCE to CLASS, the compiler will invoke JavaFileObject's getJavaFileForOutput to print class metadata which stored on JavaFileObject.
        //On print class metadata stage, will invoke openOutPutStream method to getting outPutStream, and will store class medata in ByteStream.
        Boolean result = systemJavaCompiler.getTask(null, fileManager, collector, options, null, Arrays.asList(javaFileObjectGen)).call();

        //collect compile message.
        for (Diagnostic<? extends JavaFileObject> diagnostic : collector.getDiagnostics()) {
            log.error( "\n {}", diagnostic.toString());
        }
        return result;
    }


    public static Class loadClass(String clsFullName){
        try {
            return classLoaderCG.loadClass(clsFullName);
        }catch (Throwable e){
            throw ExceptionOF.runtimeExc_.apply("Class not found : " + clsFullName);
        }
    }

    private static String jarPath(){
        String jarFile = DCUtil.class.getClassLoader().getResource("").getFile();
        int jarIdx = jarFile.indexOf(".jar");
        if(jarIdx >= 0) jarFile = jarFile.substring(0,jarIdx + 4); else return null;
        if(jarFile.startsWith("file:")) jarFile = jarFile.replaceFirst("file:","");
        return jarFile;
    }

    private static String depressPath(String jarPath){
        String depressOutputPath = null;
        int dotted = jarPath.lastIndexOf(".");
        if(dotted >= 0) depressOutputPath = jarPath.substring(0, dotted).concat("-depress");
        else depressOutputPath = jarPath.concat("-depress");
        File outPut = new File(depressOutputPath);
        if(outPut.exists()) outPut.delete(); else outPut.mkdir();
        return depressOutputPath;
    }

    private static void sourceJarCopy(String jarFilePath,String depressPath){
        File jarFile = new File(jarFilePath);
        String concat = depressPath.concat(File.separator).concat(jarFilePath.substring(jarFilePath.lastIndexOf(File.separator) + File.separator.length()));
        File depressFolder = new File(concat);
        try {
            FileCopyUtil.copy(jarFile,depressFolder);
        } catch (Throwable e) {
            log.info("\n DCUtil :: sourceJarCopy :: occur an error, check file or folder's privileges pls.");
            throw ExceptionOF.runtimeExc_.apply("DCUtil :: sourceJarCopy :: occur an error, check file or folder's privileges pls.");
        }
    }

    /**
     * This method is used for decompress jar file.
     */
    private static synchronized String decompress(){
        String fileName = jarPath();
        if(ObjectOF.isNull_.test(fileName)) return null;
        String outputPath = depressPath(fileName);
        if(ObjectOF.isNull_.test(outputPath)) return null;

        //source jar files copy.
        sourceJarCopy(fileName,outputPath);
        //reConstruct jar file path copied, depress will operate this jar file.
        int lastDisSep = fileName.lastIndexOf(File.separator);
        if(lastDisSep >= 0) fileName = outputPath.concat(File.separator).concat(fileName.substring(lastDisSep + File.separator.length()));
        else fileName = outputPath.concat(File.separator).concat(fileName);

        if (!outputPath.endsWith(File.separator)) {
            outputPath += File.separator;
        }
        File dir = new File(outputPath);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        JarFile jf = null;
        try{
            jf =  new JarFile(fileName);
            for (Enumeration<JarEntry> e = jf.entries(); e.hasMoreElements();) {
                JarEntry je = (JarEntry) e.nextElement();
                String outFileName = outputPath + je.getName();
                File f = new File(outFileName);
                if(je.isDirectory()){
                    if(!f.exists()){ f.mkdirs(); }
                }else{
                    File pf = f.getParentFile();
                    if(!pf.exists()){ pf.mkdirs(); }
                    InputStream in = jf.getInputStream(je);
                    OutputStream out = new BufferedOutputStream(new FileOutputStream(f));
                    byte[] buffer = new byte[2048];
                    int nBytes = 0;
                    while ((nBytes = in.read(buffer)) > 0) { out.write(buffer, 0, nBytes); }
                    out.flush(); out.close(); in.close();
                }
            }
        }catch(Exception e){
            log.info("\n DCUtil :: decompress JAR file occur an error. details : " + e.getMessage());
            return null;
        }finally{
            if(jf!=null){
                try {
                    jf.close();
                    File jar = new File(jf.getName());
                    if(jar.exists()){ jar.delete(); }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        File file = new File(outputPath);
        SureOF.notNull(file,"DCUtil :: decompress :: Jar Depress Output Path not exist! check user's privileges pls.");
        String[] BOOT_INF = file.list((innerFile, innerFileName) -> innerFileName.equals("BOOT-INF"));
        SureOF.notNullAll(BOOT_INF,"DCUtil :: decompress :: Jar Depress Output Path's innerDisDirs named [BOOT-INF] not exist! check user's privileges pls.");
        String BOOT_INF_DIR = outputPath.concat(BOOT_INF[0]);
        String[] LIB = new File(BOOT_INF_DIR).list((innerFile, innerFileName) -> innerFileName.equals("lib"));
        SureOF.notNullAll(LIB,"DCUtil :: decompress :: Jar Depress Output Path's innerDisDirs named [LIB] not exist! check user's privileges pls.");
        String LIB_DIR = BOOT_INF_DIR.concat(File.separator).concat(LIB[0]);
        String[] jarOrClassFile = new File(LIB_DIR).list((innerFile, innerFileName) -> innerFileName.endsWith(".jar") || innerFileName.endsWith(".class"));
        //user's cls file.
        DCUtil.classPathDepend = outputPath + "BOOT-INF" + File.separator + "classes";
        //outer depends jars.
        if(ObjectOF.notNullAll_.test(jarOrClassFile)) {
            Set<String> jarOrClsFullPath = OFUtil.aryToList(jarOrClassFile, String.class).stream().map(jarOrCls -> LIB_DIR.concat(File.separator).concat(jarOrCls)).collect(Collectors.toSet());
            if(ObjectOF.notNull_.test(jarOrClsFullPath) && jarOrClsFullPath.size() > 0)
                DCUtil.classPathDepend = DCUtil.classPathDepend.concat(":").concat(String.join(":", jarOrClsFullPath));
            return classPathDepend;
        }
        return null;
    }
}
